#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#

#
#  Multi-Processes Server Mode
#

import time,socket,fcntl,errno

import multiprocessing
import Queue


from spinage.net  import ioloop,iostream
from spinage.help import logging

from spinage.help.codecchain import CodecChain
from spinage.help import protocol

from spinage.session import Session

from spinage.response import RC_OK,RC_INERNAL_ERROR


MIN_REQUEST_ID = 1000
MAX_REQUEST_ID = 1024 * 1024 * 1024 * 2 
REQUEST_STOP   = 0

def process_main(handler, q_in, q_out) :
    myname = multiprocessing.current_process().name
    mypid = multiprocessing.current_process().pid

    logger = logging.getLogger( '%s-%s' % (myname, mypid) )

    while True :
        # REQUEST ::= REQUEST-ID + DATA
        # RESPONSE ::= CODE + REQUEST-ID + DATA
        try :
            req_id,request,address_from = q_in.get() 
        except KeyboardInterrupt :
            break

        if REQUEST_STOP == req_id :
            logger.info( 'process "%s" stop', myname )
            break

        #logger.info('WORKER get request (REQ-ID:%s REQ:%s ADDRSS-FROM:%s)', req_id, request, address_from)
        try :
            response,address_to = handler.handle(request, address_from)
            rc = RC_OK
        except :
            logger.excpt()
            rc = RC_INTERNAL_ERROR

        #logger.info('WORKER put response (RC:%s REQ-ID:%s RESPONSE:%s)', rc, req_id, response)
        q_out.put( (rc, req_id, response) )

    ## process_main()


class   Server(object) :

    def __init__(self) :
        self._logger = logging.getLogger(self.__class__.__name__)
        self._io_loop = ioloop.IOLoop()

        self._codec_chain = CodecChain()
        self._protocol_factory = None

        # handlers list
        #   fill with 2-tuple(name, handler)
        self._handlers = []

        self._q_to_parent = multiprocessing.Queue()
        self._q_to_children = []

        self._request_id = MIN_REQUEST_ID

        self._request_ongoing = {}

        self._router = None

        self._time_hot = 0

        self._addresses_listening = {}
        self._sockets_listening = {}
        ## __init__()


    def setCodecChain(self, codec_chain) :
        if codec_chain :
            self._codec_chain = codec_chain
        ## setCodecChain()

    def getCodecChain(self) :
        return self._codec_chain
        ## getCodecChain()

    def setProtocolFactory(self, protocol_factory) :
        if protocol_factory :
            self._protocol_factory = protocol_factory
        ## setProtocolFactory()

    def shutdown(self) :
        if self._io_loop.running() :
            self._io_loop.stop()

        # stop subprocess
        for _name,_q_to_child,_p in self._handlers :
            _q_to_child.put( (REQUEST_STOP,None,None) )

        # join subprocess
        for _name,_q_to_child,_p in self._handlers :
            _p.join()
        ## shutdown()

    ####-----------------------------------------------------------####

    def setHandler(self, handler, name=None) :
        '''set handler, and discard old one
        '''
        if not handler :
            return

        # append or replace named-handler
        if name :
            for i in xrange(len(self._handlers)) :
                _name,_q_to_child, _p = self._handlers[ i ]
                if _name == name :
                    # stop old process
                    _q_to_child.put( (REQUEST_STOP,None,None) )

                    # create new process
                    p = multiprocessing.Process(target=process_main, args=(handler, _q_to_child, self._q_to_parent), name=name)
                    p.start()

                    self._handlers[ i ] = (name, q_to_child, p)
                    return

        # not found, just append it
        if name is None :
            id = len(self._handlers) + 1
            name = 'W%03d' % id
        q_to_child = multiprocessing.Queue()
        p = multiprocessing.Process(target=process_main, args=(handler, q_to_child, self._q_to_parent), name=name)
        p.start()

        self._handlers.append( (name, q_to_child, p) )
        return
        ## setHandler()

    def getHandlerIdByName(self, name) :
        for i in xrange(len(self._handlers)) :
            _name,_handler = self._handlers[i]
            if _name == name :
                return i
        ## getHandlerIdByName()

    ####-----------------------------------------------------------####

    def setRouter(self, router) :
        '''set router to route message among multi-processes
        '''
        if router :
            self._router = router

            name_list = []
            for _n,_q,_p in self._handlers :
                name_list.append( _n )
            self._router.setHandlerNameList( name_list )
        ## setRouter()

    def __routeMessageTo(self, message, address_from) :
        '''route message to a handler
        '''
        id = self._router.route( message, address_from )
        return  self._handlers[ id ]
        ## routeMessageTo()

    ####-----------------------------------------------------------####

    def listen(self, address) :
        if isinstance(address, int) :
            port = address
            address = ('', port)

        if address not in self._addresses_listening :
            self.__do_listen( address )
        ## listen()
        
    def __do_listen(self, address) :
        _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
        flags = fcntl.fcntl(_socket.fileno(), fcntl.F_GETFD)
        flags |= fcntl.FD_CLOEXEC
        fcntl.fcntl(_socket.fileno(), fcntl.F_SETFD, flags)
        _socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        _socket.setblocking(0)
        _socket.bind(address)
        _socket.listen(128)
        self._io_loop.add_handler(_socket.fileno(), self.__onConnectionReady, self._io_loop.READ)

        self._addresses_listening[ address ] = _socket
        self._sockets_listening[ _socket.fileno() ] = _socket
        ## __do_listen()


    def __onConnectionReady(self, fd, events):
        if fd not in self._sockets_listening :
            return

        _socket = self._sockets_listening[ fd ]
        while True:
            try:
                connection, address = _socket.accept()
                connection.setblocking(0)
            except socket.error, e:
                if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
                    return
                raise

            self._logger.info('%s (fd:%d) is connected', address, connection.fileno())
            #stream = iostream.IOStream(connection, io_loop=self._io_loop)
            #TcpConnection(stream, address, self._request_callback)
            session = Session(address, io_loop=self._io_loop, sock=connection)
            session.setAutoConnect( False )
            session.setProtocol( self._protocol_factory.getInstance() )
            session.setCodecChain( self.getCodecChain() )
            session.recv(self.__onRequestGot)
        ## __onConnectionReady()


    def __onRequestGot(self, session, state_code, data_or_error, request=None) :
        if protocol.PC_OK != state_code :
            self._logger.error('ignore bad request : [%d] %s', state_code, data_or_error)
            return

        data = data_or_error
        n,q,p = self.__routeMessageTo( data, session.getAddress() )
        req_id = self.__getRequestId()
        q.put( (req_id, data, session.getAddress()) )
        self._request_ongoing[ req_id ] = (q, session, data)

        # time hot zone : 10 seconds
        self._time_hot = time.time() + 10.0

        ## !!! do NOT call recv here, WHY ???
        #session.recv(self.__onRequestGot)
        ## __onRequest()

    def __onResponseSent(self, session, state_code, data_or_error, request=None) :
        # if state_code is not OK, just cancel the session
        ##self._logger.info(' - onResponseSent (CODE:%s)', state_code)
        session.recv(self.__onRequestGot)
        ## __onResponseSent()

    def __getRequestId(self) :
        self._request_id += 1
        if self._request_id >= MAX_REQUEST_ID :
            self._request_id = MIN_REQUEST_ID + 1

        return self._request_id
        ## __getRequestId()

    def __scheduleCheckQueue(self) :
        time_now = time.time()
        if time_now < self._time_hot :
            time_check = time_now + 0.0001
        else :
            time_check = time_now + 0.001
        self._io_loop.add_timeout( time_check, self.__checkQueue )
        ## __scheduleCheckQueue()

    def __checkQueue(self) :
        try :
            rc,req_id,response = self._q_to_parent.get(False)
            ##self._logger.info(' - checking queue, and GOT (RC:%s REQ-ID:%s RESPONSE:%s)',rc,req_id,response)
            # send response back
            # get session from BulletinBoard
            q,session,data_ = self._request_ongoing.pop( req_id )
            session.send( response, self.__onResponseSent )
        except Queue.Empty :
            pass
        finally :
            self.__scheduleCheckQueue()
        ## __checkQueue()


    def serve_forever(self) :
        try :
            if not self._protocol_factory :
                raise RuntimeError, 'No ProtocolFactory is set for Server'
            if not self._handlers :
                raise RuntimeError, 'No handler is set for Server'
            if not self._router :
                raise RuntimeError, 'No router is set for Server'


            self.__scheduleCheckQueue()
        
            self._io_loop.start()
            ## serve_forever()
        finally :
            self.shutdown()

        self._logger.info('done.')
    ## class Server

